package flexo.file.reader;

import java.io.BufferedReader;
import java.io.IOException;

import flexo.file.*;

/**
 * Parses an Xml-like file. It's not a proper XML parser.
 */
public class Xml extends Reader
{
	enum Token
	{
		EOF,
		MIN,
		MAX,
		SLASHMAX,
		MINSLASH,
		ID,
		TEXT,
		EQUAL,
		QUOTE,
		WHITESPACE
	}
	
	protected char current;
	protected BufferedReader reader;
	protected int line = 1;
	protected String lexeme;
	
	protected char limit = ' ';
	
	protected char backupCurrent;
	protected int backupLine;
	
	protected void backup()
	{
		backupCurrent = current;
		backupLine = line;
		
		try
		{
			reader.mark(2);
		}
		catch (IOException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	protected void restore()
	{
		current = backupCurrent;
		line = backupLine;
		try
		{
			reader.reset();
		}
		catch (IOException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	protected boolean test(Token token)
	{
		try
		{
			backup();
			match(token);
			restore();
						
			return true;
		}		
		catch (Exception e)
		{
			restore();
			return false;
		}
	}
	
	protected char peek()
	{
		return current;
	}
	
	protected char read() throws IOException
	{
		char temp = current;
		current = saferead();
		if (temp == '\n')
		{
			++line;
		}
		return temp;
	}
	
	protected char saferead() throws IOException
	{
		if (reader.ready())
			return (char)reader.read();
		else
			return '~';
	}	
	
	protected void match(Token token) throws Exception
	{
		switch (token)
		{
		case EOF:
			if (reader.ready())
			{
				throw new Exception("End of file not found");
			}
			break;
		case MIN:
			if (read() != '<')
			{
				throw new Exception("'<' expected");
			}
			break;
		case MAX:
			if (read() != '>')
			{
				throw new Exception("'>' expected");
			}
			break;			
		case SLASHMAX:
			if (read() != '/' || read() != '>')
			{
				throw new Exception("'/>' expected");
			}
			break;
		case MINSLASH:
			if (read() != '<' || read() != '/')
			{
				throw new Exception("'</' expected");
			}
			break;
		case ID:
			StringBuffer sb1 = new StringBuffer();
			while (isIdCharacter(peek()))
			{
				sb1.append(read());
			}
			lexeme = sb1.toString();
			if (lexeme.length() == 0)
			{
				throw new Exception("ID expected");
			}
			break;
		case TEXT:
			StringBuffer sb2 = new StringBuffer();
			while (peek() != limit)
			{
				sb2.append(read());
			}
			lexeme = sb2.toString();
			break;
		case EQUAL:
			if (read() != '=')
			{
				throw new Exception("'=' expected");
			}
			break;
		case QUOTE:
			if (read() != '\"')
			{
				throw new Exception("'\"' expected");
			}
			break;
		case WHITESPACE:
			if (peek() == '\n')
				++line;
			while (Character.isWhitespace(peek()))
			{
				read();
			}
			break;
		}
	}
	
	protected boolean isIdCharacter(char c)
	{
		return Character.isLetterOrDigit(c) || c == '_';
	}
	
	protected Node node() throws Exception
	{
		Node n;
		// Name
		match(Token.MIN);
		match(Token.ID);
		n = new Node(lexeme);
		match(Token.WHITESPACE);
		
		// Parameters
		while (test(Token.ID))
		{
			match(Token.ID);
			String paramName = lexeme;
			match(Token.EQUAL);
			match(Token.QUOTE);
			limit = '"';
			match(Token.TEXT);
			String value = lexeme;
			match(Token.QUOTE);
			
			n.setProperty(paramName, value);
			
			match(Token.WHITESPACE);
		}
		
		// Without children
		if (test(Token.SLASHMAX))
		{
			match(Token.SLASHMAX);
		}
		// With children or text
		else
		{
			match(Token.MAX);			
			match(Token.WHITESPACE);
			
			// children
			if (!test(Token.MINSLASH))
			{
				if (peek() == '<')
				{
					while (!test(Token.MINSLASH) && peek() == '<')
					{
						n.addChild(node());
						match(Token.WHITESPACE);
					}
				}
				// text
				else
				{
					limit = '<';
					match(Token.TEXT);
					n.setProperty("text",lexeme.trim());				
				}
			}
			
			// Closing tag
			match(Token.MINSLASH);
			match(Token.ID);
			if (!lexeme.equals(n.name))
			{
				throw new Exception("Closing tag doesn't match the opening one");
			}
			match(Token.MAX);
		}
		return n;
	}
	
	protected void parse(Node n) throws Exception
	{
		read();
		
		match(Token.WHITESPACE);
		n.addChild(node());
		match(Token.WHITESPACE);
		match(Token.EOF);		
	}

	public void read(Node fileNode)
	{
		try
		{
			reader = this.getBufferedReader(fileNode);
			
			parse(fileNode);
			
			reader.close();
		}
		catch (Exception e)
		{
			formatError(fileNode.name,line);
		}
	}
}
